﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using TanmiaGrp.Basis;
using TanmiaGrp.Basis.Specification;
using TanmiaGrp.GeneralLedger.Core;
using TanmiaGrp.GeneralLedger.IRepository;
using TanmiaGrp.GeneralLedger.IRepository.Basis;

namespace TanmiaGrp.GeneralLedger.Domain
{
    public class AccountDomain : BaseDomain<Account, AccountDomain>
    {
        protected static IAccountRepository AccountRepository
        {
            get { return Repository as IAccountRepository; }
        }

        public override Account Add(Account entity)
        {
            if (entity.IsTransactional && entity.Parent.IsTransactional)
                throw new DomainException("الحساب الفرعي يجب أن يتبع لحساب رئيسي فقط.");

            if (!entity.IsTransactional && entity.Parent.IsTransactional)
                throw new DomainException("لا يمكن لحساب رئيسي أن يتبع لحساب فرعي");

            return base.Add(entity);
        }

        public override Account Update(Account entity)
        {
            if (entity == null)
                throw new DomainException("يجب أن لايكون الكائن فارغاً");

            return Repository.Update(entity);
        }

        /// <summary>
        /// Retrieves the whole chart of all accounts as a tree represented by 
        /// List of all Parent == null then by their Children.
        /// </summary>
        /// <returns></returns>
        public List<Account> GetChartOfAccounts(SearchSpecification searchSpecs)
        {
            searchSpecs.ParentAccountID = null;

            List<Account> parents = QueryRepository.LoadAll<Account, string>(CreateSpecification(searchSpecs), o => o.AccountNumber);
            foreach (Account parentAccount in parents)
                parentAccount.AccountChildren = GetChildren(parentAccount, searchSpecs);

            return parents;
        }

        private List<Account> GetChildren(Account account, SearchSpecification searchSpecs)
        {
            searchSpecs.ParentAccountID = account.AccountID;
            List<Account> children = QueryRepository.LoadAll<Account, string>(CreateSpecification(searchSpecs), o => o.AccountNumber);

            foreach (Account acc in children)
            {
                acc.AccountChildren = GetChildren(acc, searchSpecs);
                acc.Parent = account;
            }

            return children;
        }

        public override List<Account> SearchPage(SearchSpecification searchSpecs)
        {
            Contract.Assert(searchSpecs != null, "searchSpecs must not be null.");

            return GetChartOfAccounts(searchSpecs);
        }

        /// <summary>
        /// Retrieves all Account that IsTransactional == false.
        /// </summary>
        /// <returns></returns>
        public List<Account> GetNonTransactionalAccounts()
        {
            GenericFetchStrategy<Account> fetch = new GenericFetchStrategy<Account>();
            //fetch.Include(x => x.AccountGroups);
            return QueryRepository.LoadAll<Account, string>(new GenericSpecification<Account>(p => p.IsTransactional == false, fetch), o => o.AccountNumber);
        }

        /// <summary>
        /// Calculates and retrives the new account number that will be used when user request
        /// to add new account as a child of another account.
        /// </summary>
        /// <param name="account">Parent account that will has a new account as a child.</param>
        /// <returns>NewAccountNumber if we can expect it, string.Empty otherwise.</returns>
        public string GetNewAccountNumber(Account account)
        {
            if (account.IsTransactional)
                throw new DomainException("لا يمكن إنشاء حساب فرعي تحت حساب فرعي آخر.");

            string newAccountNumber = string.Empty;

            // if account has no children lastChild will be null.
            var accountChildren =
                account.AccountChildren == null || account.AccountChildren.Count == 0
                ? QueryRepository.LoadAll<Account, string>(new GenericSpecification<Account>(p => p.ParentAccountID == account.AccountID), o => o.AccountNumber)
                : account.AccountChildren;

            Account lastChild = accountChildren
                .OrderBy(x => x.AccountNumber)
                .LastOrDefault();

            if (lastChild != null)
            {
                int result = 0;

                // if accountNumber has any things except number we cannot expect the next 
                // account number.
                if (int.TryParse(lastChild.AccountNumber, out result))
                {
                    result++;
                    newAccountNumber = result.ToString();

                    // Length(newAccountNumber) must no be greater of Length(lastChild.AccountNumber)
                    // because then the account children count is being exceeded.
                    if (newAccountNumber.Length != lastChild.AccountNumber.Length)
                        newAccountNumber = string.Empty;
                }
            }
            else // if account has no childern
            {
                // specify first child account number as a default.
                newAccountNumber = string.Format("{0}0001", account.AccountNumber);
            }

            return newAccountNumber;
        }

        /// <summary>
        /// Calculates and retrives the new account level that will used when user request
        /// to add new account as a child of another account.
        /// </summary>
        /// <param name="account">Parent account that will has a new account as a child.</param>
        /// <returns>New level of account.</returns>
        public int GetNewAccountLevel(Account account)
        {
            if (account.IsTransactional)
                throw new DomainException("لا يمكن إنشاء حساب فرعي تحت حساب فرعي آخر.");

            // because the child account's level is deeper than the parent with one level.
            return account.Level + 1;
        }

        /// <summary>
        /// Create new child account that belong to the parent.
        /// </summary>
        /// <param name="parent"></param>
        /// <returns></returns>
        public Account CreateNewChild(Account parent)
        {
            if (parent == null)
                throw new DomainException("يجب أن تحديد الحساب الأب قبل إنشاء حساب جديد");

            if (parent.IsTransactional)
                throw new DomainException("لا يمكن إنشاء حساب فرعي تحت حساب فرعي آخر.");

            return new Account
            {
                AccountNumber = GetNewAccountNumber(parent),
                Level = GetNewAccountLevel(parent),
                Parent = parent,
                IsTransactional = true,
                IsEnabled = true,
                CreateDate = DateTime.Now
            };
        }

        public Account CreateNew()
        {
            return new Account
            {
                IsTransactional = false,
                IsEnabled = true,
                CreateDate = DateTime.Now
            };
        }

        /// <summary>
        /// Retrieves all Accounts that IsTransactional == true.
        /// </summary>
        /// <returns></returns>
        public List<Account> GetTransactionalAccounts()
        {
            return QueryRepository.LoadAll<Account, string>(new GenericSpecification<Account>(p => p.IsTransactional), o => o.AccountNumber);
        }

        /// <summary>
        /// Update the balances of Chart of Account of the passed account.
        /// </summary>
        /// <param name="account"></param>
        /// <param name="debit">المدين</param>
        /// <param name="credit">الدائن</param>
        /// <param name="uow"></param>
        /// <returns></returns>
        internal bool UpdateBalancesTree(Account account, double debit, double credit, IUnitOfWork uow)
        {
            Contract.Assert(uow != null, "UnitOfWork must not be null when UpdatingBalancesTree.");
            Contract.Assert(account != null, "account must not be null.");

            if (account == null)
                throw new DomainException("الحساب الذي ترحيل الأرصدة عليه يجب أن لا يكون فارغ.");

            if (credit == 0 && debit == 0)
                throw new DomainException("طرفي القيد يجب أن لا يكونا صفر");

            if (credit > 0 && debit > 0)
                throw new DomainException("لا يمكن لطرفي القيد أن يحتويا على مبلغ");

            if (account.IsEnabled == false)
                throw new DomainException(string.Format("الحساب {0} غير مفعل لذلك لايمكن إجراء أي عملية عليه", account));

            if (credit > 0 & account.IsCreditable == false)
                throw new DomainException(string.Format("الحساب {0} لايمكن أن يكون بالطرف الدئن لأنه لا يسمح بذلك", account));

            if (debit > 0 & account.IsDebitable == false)
                throw new DomainException(string.Format("الحساب {0} لايمكن أن يكون بالطرف المدين لأنه لا يسمح بذلك", account));

            if (account.ExpiryDate != null && account.ExpiryDate <= DateTime.Today)
                throw new DomainException(string.Format("الحساب {0} منتهي الصلاحية لذلك لايمكن أن يكون طرف في القيد", account));

            if (account.IsTransactional == false)
                throw new DomainException(string.Format("الحساب {0} حساب رئيسي لذلك لايمكن أن تتم عليه أي عمليات", account));

            Account pointer = account;
            while (pointer != null)
            {
                Account tempAcc = uow.QueryRepository.Single<Account>(x => x.AccountNumber == pointer.AccountNumber);
                if (tempAcc.MaxCredit != null && tempAcc.TotalCredit + credit > tempAcc.MaxCredit)
                    throw new DomainException(string.Format("الحساب {0} تجاوز الاحد الأعلى للدائن, لذلك لايمكن أتمام العملية", tempAcc));

                if (tempAcc.MaxDebit != null && tempAcc.TotalDebit + debit > tempAcc.MaxDebit)
                    throw new DomainException(string.Format("الحساب {0} تجاوز الاحد الأعلى للمدين, لذلك لايمكن اتمام العملية", tempAcc));

                tempAcc.TotalCredit += credit;
                tempAcc.TotalDebit += debit;
                tempAcc.Balance = tempAcc.TotalDebit - tempAcc.TotalCredit;
                tempAcc.LastOperationDate = DateTime.Now;
                uow.Repository.Update<Account>(tempAcc);

                pointer = tempAcc.Parent;
            }

            return true;
        }

        private GenericSpecification<Account> CreateSpecification(SearchSpecification searchSpecs)
        {
            Expression<Func<Account, bool>> predicate = x => searchSpecs.ShowIsDisabled ? true : x.IsEnabled == true;

            predicate = x => (searchSpecs.ShowIsDisabled ? true : x.IsEnabled == true)
                && (x.ParentAccountID == searchSpecs.ParentAccountID);

            GenericFetchStrategy<Account> fetch = new GenericFetchStrategy<Account>();
            fetch.Include(x => x.AccountGroups);

            return new GenericSpecification<Account>(predicate, fetch);
        }
    }
}
